// FeatureProcessor.ts
export interface FeatureData {
  spectral_centroids: number[];
  spectral_rolloff: number[];
  spectral_bandwidth: number[];
  zero_crossing_rate: number[];
  rms: number[];
  mfccs: number[][];
}

export interface Features {
  duration: number;
  fps: number;
  frames: number;
  tempo: number;
  features: FeatureData;
}

export interface VisualizationEvent {
  time: number;
  spectralCentroid: number;
  spectralRolloff: number;
  spectralBandwidth: number;
  zeroCrossingRate: number;
  rms: number;
  mfccs: number[];
}

export class FeatureProcessor {
  private features: Features | null = null;
  private subscribers: ((event: VisualizationEvent) => void)[] = [];
  private audio: HTMLAudioElement | null = null;
  private isPlaying: boolean = false;

  constructor() {
    // Constructor is empty now, features will be loaded asynchronously
  }

  // Load features data from JSON file with type validation
  public async loadFeatures(): Promise<void> {
    try {
      const response = await fetch('./data/gthljxq.feature.json');
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      const data = await response.json();
      
      // Basic type check to ensure we have the expected structure
      if (!data || typeof data.duration !== 'number' || !data.features) {
        throw new Error('Invalid feature data format');
      }
      
      this.features = data as Features;
      console.log('Feature data loaded and validated successfully');
    } catch (error) {
      console.error('Failed to load or validate features data:', error);
      throw error;
    }
  }

  // Initialize audio playback
  public initAudio(): void {
    this.audio = new Audio('./data/gthljxq.mp3');
    this.audio.addEventListener('play', () => {
      this.isPlaying = true;
      this.update();
      console.log('Audio playback started');
    });
    
    this.audio.addEventListener('pause', () => {
      this.isPlaying = false;
      console.log('Audio playback paused');
    });
    
    this.audio.addEventListener('ended', () => {
      this.isPlaying = false;
      console.log('Audio playback ended');
    });
    
    console.log('Audio initialized');
  }

  // Play audio
  public play(): void {
    if (!this.features || !this.audio) {
      console.error('Features or audio not initialized');
      return;
    }
    
    this.audio.play().catch((error) => {
      console.error('Error playing audio:', error);
      // Optionally, show a user-friendly message in the UI
    });
  }

  // Pause audio
  public pause(): void {
    if (!this.audio) {
      console.error('Audio not initialized');
      return;
    }
    
    this.audio.pause();
  }

  // Seek to a specific time
  public seekTo(time: number): void {
    if (!this.features || !this.audio) {
      console.error('Features or audio not initialized');
      return;
    }
    
    this.audio.currentTime = time;
    this.emitEvent(time);
    console.log(`Seeked to time: ${time}`);
  }

  // Get current time
  public getCurrentTime(): number {
    if (!this.audio) return 0;
    return this.audio.currentTime;
  }

  // Get duration
  public getDuration(): number {
    if (!this.features) return 0;
    return this.features.duration;
  }

  // Subscribe to visualization events
  public subscribe(callback: (event: VisualizationEvent) => void): void {
    this.subscribers.push(callback);
    console.log('New subscriber added');
  }

  // Unsubscribe from visualization events
  public unsubscribe(callback: (event: VisualizationEvent) => void): void {
    const index = this.subscribers.indexOf(callback);
    if (index > -1) {
      this.subscribers.splice(index, 1);
      console.log('Subscriber removed');
    }
  }

  // Update loop for animation frame
  private update = (): void => {
    if (!this.isPlaying || !this.features || !this.audio) return;

    const currentTime = this.audio.currentTime;
    this.emitEvent(currentTime);

    // Continue the animation loop only if still playing
    if (this.isPlaying) {
      requestAnimationFrame(this.update);
    }
  };

  // Emit visualization event to all subscribers
  private emitEvent(currentTime: number): void {
    if (!this.features) return;
    
    // Calculate the corresponding frame index based on current time and FPS
    const frameIndex = Math.floor(currentTime * this.features.fps);
    
    // Prevent out-of-bounds access
    if (frameIndex < 0 || frameIndex >= this.features.frames) {
      return;
    }

    // Extract feature values for the current frame, providing defaults if undefined
    const event: VisualizationEvent = {
      time: currentTime,
      spectralCentroid: this.getSafeFeatureValue(this.features.features.spectral_centroids, frameIndex),
      spectralRolloff: this.getSafeFeatureValue(this.features.features.spectral_rolloff, frameIndex),
      spectralBandwidth: this.getSafeFeatureValue(this.features.features.spectral_bandwidth, frameIndex),
      zeroCrossingRate: this.getSafeFeatureValue(this.features.features.zero_crossing_rate, frameIndex),
      rms: this.getSafeFeatureValue(this.features.features.rms, frameIndex),
      mfccs: this.features.features.mfccs.map(coefficients => 
        this.getSafeFeatureValue(coefficients, frameIndex)
      )
    };

    // Notify all subscribers safely
    this.subscribers.forEach((callback) => {
      try {
        callback(event);
      } catch (error) {
        console.error('Error in visualization event subscriber:', error);
      }
    });
  }

  /**
   * Helper method to safely get a value from an array at a given index.
   * Returns 0 if the array is falsy, the index is out of bounds, or the value is undefined.
   */
  private getSafeFeatureValue(array: number[] | undefined, index: number): number {
    if (!array || index < 0 || index >= array.length) {
      return 0;
    }
    return array[index] ?? 0;
  }

  /**
   * Clean up resources, such as removing event listeners.
   * Should be called when the component using this processor is destroyed.
   */
  public destroy(): void {
    if (this.audio) {
      this.audio.removeEventListener('play', this.onPlay);
      this.audio.removeEventListener('pause', this.onPause);
      this.audio.removeEventListener('ended', this.onEnded);
      this.audio = null;
    }
    this.subscribers = [];
    this.isPlaying = false;
    console.log('FeatureProcessor destroyed and cleaned up');
  }

  // Bound event handlers to maintain correct 'this' context
  private onPlay = () => {
    this.isPlaying = true;
    this.update();
    console.log('Audio playback started');
  };

  private onPause = () => {
    this.isPlaying = false;
    console.log('Audio playback paused');
  };

  private onEnded = () => {
    this.isPlaying = false;
    console.log('Audio playback ended');
  };
}

// Initialize audio playback
public initAudio(): void {
  this.audio = new Audio('./data/gthljxq.mp3');
  this.audio.addEventListener('play', this.onPlay);
  this.audio.addEventListener('pause', this.onPause);
  this.audio.addEventListener('ended', this.onEnded);
  console.log('Audio initialized');
}